package joe;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.stb.STBTTFontinfo;
import static org.lwjgl.stb.STBTruetype.*;
import org.lwjgl.system.MemoryUtil;

public final class Font {
	private static final int GAP = 1;
	private final @NotNull String fontPath;
	private final long fontInfoPtr, bmpBufPtr;
	private final float scale;
	private final int pixelSize, baseline, spaceAdv;
	private final int textureSize;
	private final short[] charInfoIndexes = new short[0x10000];
	private final HashMap<Integer, Integer> extraCharInfoIndexMap = new HashMap<>();
	private long @NotNull [] charInfos = new long[128]; // maybe realloc
	private int charInfoCount, texX, texY, texMaxH;
	private final Render.Texture2D texture;

	Font(@Nullable Render render, @NotNull String fontPath, int pixelSize, int textureSize) {
		var mbb = ResourceMapFile.loadMapFile(fontPath);
		if (mbb == null)
			throw new IllegalArgumentException("not found font file: " + fontPath);
		long fiPtr = 0;
		try {
			int fiSize = (STBTTFontinfo.SIZEOF + 7) & ~7; // 8-byte aligned
			int bbSize = ((pixelSize + 3) & ~3) * pixelSize; // 4-byte aligned width
			fiPtr = UnsafeUtil.allocateMemory(fiSize + bbSize);
			if (nstbtt_InitFont(fiPtr, MemoryUtil.memAddress(mbb), 0) == 0)
				throw new IllegalStateException("init font failed: " + fontPath);
			this.fontPath = fontPath;
			fontInfoPtr = fiPtr;
			bmpBufPtr = fiPtr + fiSize;
			scale = nstbtt_ScaleForPixelHeight(fiPtr, pixelSize);
			this.pixelSize = pixelSize;
			var tmpBuf = Engine.current().getTmpBuf();
			nstbtt_GetFontVMetrics(fiPtr, tmpBuf.ptr(), 0, 0);
			baseline = Math.round(tmpBuf.getInt() * scale);
			nstbtt_GetCodepointHMetrics(fiPtr, ' ', tmpBuf.ptr(), 0);
			spaceAdv = Math.round(tmpBuf.getInt() * scale);
			this.textureSize = textureSize;
			Arrays.fill(charInfoIndexes, (short)-1);
			texture = render != null ? render.createTexture2D(textureSize, textureSize, 1, 0, 0) : null;
		} catch (Throwable e) { // rethrow
			if (fiPtr != 0)
				UnsafeUtil.freeMemory(fiPtr);
			UnsafeUtil.freeMappedByteBuffer(mbb);
			throw e;
		}
		var fiPtr0 = fiPtr;
		Cleaner.addGlobal(this, () -> {
			UnsafeUtil.freeMemory(fiPtr0);
			UnsafeUtil.freeMappedByteBuffer(mbb);
		});
	}

	public @NotNull String getFontPath() {
		return fontPath;
	}

	public int getPixelSize() {
		return pixelSize;
	}

	public int getBaseline() {
		return baseline;
	}

	public @NotNull Render.Texture2D getTexture() {
		return texture;
	}

	public boolean hasCodePoint(int codePoint) {
		return nstbtt_FindGlyphIndex(fontInfoPtr, codePoint) != 0;
	}

	public long getOrAddCharInfo(int codePoint) {
		final int index;
		if (codePoint <= 0xffff)
			index = charInfoIndexes[codePoint] & 0xffff;
		else {
			final var v = extraCharInfoIndexMap.get(codePoint);
			index = v != null ? v : 0xffff;
		}
		if (index != 0xffff)
			return charInfos[index];
		if (charInfoCount == 0xffff)
			throw new IllegalStateException("charInfoCount overflow");

		final var tmpBuf = Engine.current().getTmpBuf();
		nstbtt_GetCodepointHMetrics(fontInfoPtr, codePoint, tmpBuf.ptr(), 0);
		final int adv = Math.round(tmpBuf.getInt() * scale);
		Objects.checkIndex(adv, 256);
		nstbtt_GetCodepointBitmapBox(fontInfoPtr, codePoint, scale, scale,
				tmpBuf.ptr(), tmpBuf.ptr(4), tmpBuf.ptr(8), tmpBuf.ptr(12));
		final int x0 = tmpBuf.getInt();
		int y0 = tmpBuf.getInt(1);
		final int w = Math.min(tmpBuf.getInt(2) - x0, pixelSize);
		final int h = Math.min(tmpBuf.getInt(3) - y0, pixelSize);
		y0 += baseline;
		Objects.checkIndex(x0 + 128, 256);
		Objects.checkIndex(y0 + 128, 256);
		Objects.checkIndex(w, 256);
		Objects.checkIndex(h, 256);
		nstbtt_MakeCodepointBitmap(fontInfoPtr, bmpBufPtr, w, h, (w + 3) & ~3, scale, scale, codePoint);

		if (texX + w + GAP > textureSize) {
			texX = 0;
			texY += texMaxH + GAP;
			Objects.checkIndex(texY + h, textureSize);
			texMaxH = h;
		} else if (texMaxH < h) {
			texMaxH = h;
			Objects.checkIndex(texY + h, textureSize);
		}
		if (texture != null)
			texture.putData(texX, texY, w, h, bmpBufPtr);
		final var v = texX + ((long)texY << 12) + ((long)w << 24) + ((long)h << 32)
					  + ((long)adv << 40) + ((long)(x0 & 0xff) << 48) + ((long)y0 << 56);
		texX += w + GAP;
		if (charInfoCount >= charInfos.length)
			charInfos = Arrays.copyOf(charInfos, charInfos.length * 2);
		if (codePoint <= 0xffff)
			charInfoIndexes[codePoint] = (short)charInfoCount;
		else
			extraCharInfoIndexMap.put(codePoint, charInfoCount);
		charInfos[charInfoCount++] = v;
		return v;
	}

	// 0[12] bit: char bitmap in texture coord x(left->right) by pixel [0,4095]
	public static int toTexX(long v) {
		return (int)(v & 0xfff);
	}

	// 12[12] bit: char bitmap in texture coord y(top->down) by pixel [0,4095]
	public static int toTexY(long v) {
		return (int)((v >> 12) & 0xfff);
	}

	// 24[8] bit: char bitmap in texture size width by pixel [0,255]
	public static int toTexW(long v) {
		return (int)((v >> 24) & 0xff);
	}

	// 32[8] bit: char bitmap in texture size height by pixel [0,255]
	public static int toTexH(long v) {
		return (int)((v >> 32) & 0xff);
	}

	// 40[8] bit: x-advance to next char base position [0,255] (not include kern between chars: nstbtt_GetCodepointKernAdvance)
	public static int toAdvanceX(long v) {
		return (int)((v >> 40) & 0xff);
	}

	// 48[8] bit: char bitmap draw offset x(left->right) by pixel [-128,127]
	public static int toOffsetX(long v) {
		return (int)(byte)(v >> 48);
	}

	// 56[8] bit: char bitmap draw offset y(top->down) by pixel [-128,127]
	public static int toOffsetY(long v) {
		return (int)(v >>> 56);
	}

	public @NotNull TextBox createTextBox(int initialCharCount) {
		return new TextBox(initialCharCount);
	}

	public final class TextBox {
		private final @NotNull Render.Mesh mesh;
		private int width;
		private int height;
		private int vertexCount;
		private int lines;
		private int lastX;

		TextBox(int initialCharCount) {
			var engine = Engine.current();
			var render = engine.getRender();
			mesh = render.createMesh(Render.TRIANGLES, render.createVertexBuffer("s2us2", 8, 0,
					(long)initialCharCount * 4 * 8), engine.getQuadsVertexIndex());
		}

		/**
		 * @return layout(location = 0) in vec2 vPos; layout(location=1) in vec2 vTex;
		 */
		public @NotNull Render.Mesh mesh() {
			return mesh;
		}

		public int width() {
			return width;
		}

		/**
		 * @return 0:top
		 */
		public int height() {
			return height;
		}

		/**
		 * @return charCount * 6
		 */
		public int vertexCount() {
			return vertexCount;
		}

		public int lines() {
			return lines;
		}

		public boolean limited() {
			return lastX < 0;
		}

		/**
		 * @param limitWidth <=0: no limit
		 * @param limitLines <=0: no limit
		 */
		public void set(@NotNull String text, int limitWidth, int limitLines) {
			final int textLen = text.length();
			if (textLen > Engine.MAX_QUADS_IN_MESH)
				throw new IllegalStateException("too many chars in text: " + textLen);
			final int maxWidth = limitWidth > 0 ? limitWidth : Integer.MAX_VALUE;
			final int maxLines = limitLines > 0 ? limitLines : Integer.MAX_VALUE;
			width = 0;
			height = 0;
			vertexCount = 0;
			lines = 0;
			lastX = 0;
			final var tmpPtr = Engine.current().getTmpBuf().ptr() + 32; // reserve some for getOrAddCharInfo
			final var ptr = textLen <= Engine.TMP_SIZE / 32 - 1 ? tmpPtr : UnsafeUtil.allocateMemory(textLen * 4 * 8L);
			try {
				text.codePoints().forEach(codePoint -> {
					if (lines >= maxLines)
						return;
					int x = lastX;
					final long charInfo;
					int adv;
					if (codePoint <= ' ') {
						charInfo = 0;
						if (codePoint == ' ')
							adv = spaceAdv;
						else if (codePoint == '\n')
							adv = Integer.MAX_VALUE - x + 1;
						else
							adv = 0;
					} else {
						charInfo = getOrAddCharInfo(codePoint);
						adv = toAdvanceX(charInfo);
					}
					if (x + adv - 1 >= maxWidth) {
						if (width < x)
							width = x;
						height += pixelSize;
						if (++lines >= maxLines) {
							lastX = -1;
							return;
						}
						x = 0;
						if (charInfo == 0)
							adv = 0;
					}
					lastX = x + adv;
					if (charInfo != 0) {
						x += toOffsetX(charInfo);
						final int y = height + toOffsetY(charInfo);
						final int tx = toTexX(charInfo);
						final int ty = toTexY(charInfo);
						final int tw = toTexW(charInfo);
						final int th = toTexH(charInfo);
						final int tx1 = tx + tw;          // 0 +---> +x [0,32767]
						final int ty1 = ty + th;          //   |
						final var off = vertexCount * 8L; //   V -y [-32768,0]
						UnsafeUtil.putShort(ptr + off, (short)x, (short)-(y + th), (short)tx, (short)ty1);
						UnsafeUtil.putShort(ptr + off + 8, (short)x, (short)-y, (short)tx, (short)ty);
						UnsafeUtil.putShort(ptr + off + 16, (short)(x + tw), (short)-(y + th), (short)tx1, (short)ty1);
						UnsafeUtil.putShort(ptr + off + 24, (short)(x + tw), (short)-y, (short)tx1, (short)ty);
						vertexCount += 4;
					}
				});
				final int x = lastX;
				if (x > 0) {
					if (width < x)
						width = x;
					height += pixelSize;
					lines++;
				}
				mesh.getVertexBuffer().putData(0, ptr, vertexCount * 8L);
				vertexCount = vertexCount / 2 * 3;
			} finally {
				if (ptr != tmpPtr)
					UnsafeUtil.freeMemory(ptr);
			}
		}
	}
}
